﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;

namespace BGShell.PowerShell.Runspaces
{
    internal sealed class BGShellPipeline
    {
        private readonly Action<BGShellPipeline> _callback;
        
        private IList<Command> _commands = new Collection<Command>();
        private IList<Object> _input = new Collection<Object>();

        private Object _userState;
        private Boolean _stopped;
        
        private String _commandLine;
        private Boolean _addToHistory;

        private Pipeline _pipeline;
        private ErrorRecord _failure;
        private Collection<Object> _errors;
        private Collection<PSObject> _results;

        private EventHandler _onDisposed;

        public event EventHandler PipelineDisposed
        {
            add { _onDisposed += value; }
            remove { _onDisposed -= value; } 
        }

        public BGShellPipeline()
        {
        }

        public BGShellPipeline(Command command)
        {
            _commands.Add(command);
        }

        public BGShellPipeline(Action<BGShellPipeline> callback)
        {
            _callback = callback;
        }

        public BGShellPipeline(String commandLine, Boolean addToHistory)
        {
            _commandLine = commandLine;
            _addToHistory = addToHistory;
        }

        public BGShellPipeline(ScriptBlock scriptBlock)
        {
            Commands.Add(new Command(scriptBlock.ToString(), true));
        }

        public override string ToString()
        {
            var result = new StringBuilder(_commandLine);

            for (int i = 0; i < _commands.Count; i++)
            {
                if (result.Length > 0)
                {
                    result.Append(" | ");
                }

                result.Append(_commands[i].ToString());
            }

            return result.ToString();
        }

        public Boolean AddToHistory
        {
            get { return _addToHistory; }
        }

        public String CommandLine
        {
            get { return _commandLine; }
        }

        public ICollection<Command> Commands
        {
            get { return _commands; }
        }

        public ICollection<Object> Input
        {
            get { return _input; }
        }

        public ICollection<PSObject> Output
        {
            get { return _results; }
        }

        public ICollection<Object> Errors
        {
            get { return _errors; }
        }

        public ErrorRecord FailureReason
        {
            get { return _failure; }
        }

        public Boolean IsStopped
        {
            get { return _stopped; }
        }

        public Object UserState
        {
            get { return _userState; }
            set { _userState = value; }
        }

        internal void OnQueued()
        {
            if (!_commands.IsReadOnly)
            {
                _commands = new ReadOnlyCollection<Command>(_commands);
            }

            if (!_input.IsReadOnly)
            {
                _input = new ReadOnlyCollection<Object>(_input);
            }
        }


        internal void Invoke(Runspace runspace, Boolean nested)
        {
            if (_stopped)
            {
                // OnPipelineDisposed was called on thread that
                // called StopAsync

                return;
            }

            if (runspace.RunspaceStateInfo.State != RunspaceState.Opened)
            {
                return;
            }

            if (nested)
            {
                InvokeNested(runspace);
            }
            else
            {
                InvokeAsync(runspace);
            }
        }

        internal void StopAsync()
        {
            if (!_stopped)
            {
                _stopped = true;

                if (_pipeline == null)
                {
                    OnPipelineDisposed(EventArgs.Empty);
                }
                else
                {
                    _pipeline.StopAsync();
                }
            }
        }

        private void InvokeNested(Runspace runspace)
        {
            _pipeline = CreateNestedPipeline(runspace);

            foreach (var cmd in Commands)
            {
                _pipeline.Commands.Add(cmd);
            }

            try
            {
                _results = _pipeline.Invoke(Input);
                _errors = _pipeline.Error.ReadToEnd();
            }
            catch (RuntimeException exc)
            {
                _failure = exc.ErrorRecord;
            }
            finally
            {
                if (_failure == null)
                {
                    _failure = GetErrorRecord(_pipeline.PipelineStateInfo.Reason);
                }

                DisposePipeline();
                OnPipelineDisposed(EventArgs.Empty);

                if (!_stopped)
                {
                    InvokeCallback();
                }
            }
        }

        private void InvokeAsync(Runspace runspace)
        {
            _pipeline = CreatePipeline(runspace);
            _pipeline.StateChanged += OnPipelineStateChanged;

            foreach (var cmd in Commands)
            {
                _pipeline.Commands.Add(cmd);
            }

            _pipeline.Input.Write(_input, true);
            _pipeline.Input.Close();

            _pipeline.InvokeAsync();
        }

        private Pipeline CreatePipeline(Runspace runspace)
        {
            if (CommandLine != null)
            {
                return runspace.CreatePipeline(CommandLine, AddToHistory);
            }

            return runspace.CreatePipeline();
        }

        private Pipeline CreateNestedPipeline(Runspace runspace)
        {
            if (CommandLine != null)
            {
                return runspace.CreateNestedPipeline(CommandLine, AddToHistory);
            }

            return runspace.CreateNestedPipeline();
        }

        private void OnPipelineDisposed(EventArgs e)
        {
            var handler = _onDisposed;
            _onDisposed = null;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnPipelineStateChanged(object sender, PipelineStateEventArgs e)
        {
            if ((e.PipelineStateInfo.State == PipelineState.Completed) ||
                (e.PipelineStateInfo.State == PipelineState.Stopped) ||
                (e.PipelineStateInfo.State == PipelineState.Failed))
            {
                _failure = GetErrorRecord(e.PipelineStateInfo.Reason);

                _results = _pipeline.Output.ReadToEnd();
                _errors = _pipeline.Error.ReadToEnd();

                DisposePipeline();
                OnPipelineDisposed(EventArgs.Empty);

                if (!_stopped)
                {
                    InvokeCallback();
                }
            }
        }

        private void DisposePipeline()
        {
            if (_pipeline != null)
            {
                _pipeline.StateChanged -= OnPipelineStateChanged;
                _pipeline.Dispose();
            }

            _pipeline = null;
        }

        private void InvokeCallback()
        {
            if (_callback != null)
            {
                _callback(this);
            }
        }

        private static ErrorRecord GetErrorRecord(Exception exc)
        {
            var containsErrorRecord = exc as IContainsErrorRecord;

            if (containsErrorRecord != null)
            {
                return containsErrorRecord.ErrorRecord;
            }

            if (exc != null)
            {
                return new ErrorRecord(exc, "BGShell.ReportException", ErrorCategory.NotSpecified, null);
            }

            return null;
        }
    }
}
